﻿#region 참조 네임스페이스

using System;
using System.IO;
using System.Net;
using System.Text;

#endregion

namespace Skying.Net.Sockets
{
	/// <summary>
	/// <see cref="System.Net.Sockets.Socket.BeginAccept"/>에 의해 전달되는 첫 번째 데이터의 내용으로 분석과 변환을 지원한다.
	/// </summary>
	/// <remarks>
	/// <para><see cref="TcpListener"/>는 연결된 클라이언트로부터 전달되는 첫 번째 데이터 내용을 분석하여 자신과 비교한다.</para>
	/// <para>분석이 실패하거나 자신과 다른 내용이 전달된 경우, 클라이언트와의 연결을 닫는다.</para>
	/// <para><see cref="TcpClient"/>는 <see cref="TcpListener"/>로 연결(<see cref="System.Net.Sockets.Socket.BeginConnect"/>)하고 <see cref="System.Net.Sockets.Socket.EndConnect"/>으로 콜백된 이후 연결 수용 조건을 반드시 전달해야 한다.</para>
	/// <para>그렇게 하지 않으면, 연결되지 않으며 양자 간의 통신은 불가능하다.</para>
	/// </remarks>
	internal class AcceptCondition
	{
		#region 상수

		/// <summary>
		/// ACCEPT 패킷의 헤더 상수를 나타낸다.
		/// </summary>
		private static readonly byte[] _ACCEPT_HEADER = new byte[] { 0xFF, 0xFF, 0xFF };

		/// <summary>
		/// 소켓 아이디의 길이 상수를 나타낸다.
		/// </summary>
		private const int _ID_LENGTH = 36;

		#endregion

		#region 프로퍼티

		/// <summary>
		/// 소켓의 고유 코드 정보를 가져온다.
		/// </summary>
		internal Guid Id
		{
			get;
			private set;
		}

		/// <summary>
		/// <see cref="Skying.Net.BaseSocket"/>이 메모리에 배열을 정렬하는 방식을 가져온다.
		/// </summary>
		/// <remarks>
		/// <para>이 Endian 열거자는 클라이언트와 리스너 간의 <see cref="Skying.Net.Sockets.Header"/> 정보를 교환할 때 사용된다.</para>
		/// <para>패킷 중 Body 영역에는 관여하지 않으므로 유의해야 한다.</para>
		/// </remarks>
		internal Endians Endian
		{
			get;
			private set;
		}

		/// <summary>
		/// 사용자 정의 데이터를 가져온다.
		/// </summary>
		internal string UserToken
		{
			get;
			private set;
		}

		#endregion

		#region 생성자/소멸자

		internal AcceptCondition(Guid id, Endians endian, string userToken)
		{
			this.Id = id;

			this.Endian = endian;

			this.UserToken = userToken;
		}

		#endregion

		#region 분석

		/// <summary>
		/// 바이트 배열을 분석하여 <see cref="Skying.Net.Sockets.AcceptCondition"/>을 추출한다.
		/// </summary>
		/// <param name="data"><see cref="Skying.Net.Sockets.AcceptCondition"/>에 대한 바이너리를 설정한다.</param>
		/// <param name="condition">주어진 바이너리를 <see cref="Skying.Net.Sockets.AcceptCondition"/>로 변환하여 참조한다.</param>
		/// <param name="clientType">클라이언트 유형 정보를 참조한다.</param>
		/// <returns>성공적으로 <see cref="Skying.Net.Sockets.AcceptCondition"/>를 추출한 경우 true, 그렇지 않으면 false가 반환된다.</returns>
		internal static bool Parse(byte[] data, out AcceptCondition condition, out ClientType clientType)
		{
			Guid id = new Guid();

			try
			{
				int offset = 0;

				if (data.Find(_ACCEPT_HEADER) < 0)
				{
					condition = null;

					// Accept 데이터에 HTTP 프로토콜 정보가 포함되어 있는 경우
					if (Encoding.UTF8.GetString(data).Contains("HTTP"))
					{
						clientType = ClientType.HTTP;
					}
					else
					{
						clientType = ClientType.Unknown;
					}

					return false;
				}

				offset += _ACCEPT_HEADER.Length;
				
				// 소켓 아이디를 읽어온다.
				string idString = Encoding.UTF8.GetString(data, offset, _ID_LENGTH);

				if (!Guid.TryParse(idString, out id))
				{
					throw new AcceptConditionParseException("Client Id is invalid. This connection will be closed.", AcceptConditionParseResult.InvalidId);
				}

				offset += _ID_LENGTH;

				// Endian 유형을 읽어온다.
				Endians endian = (Endians)data[offset];

				if (!Enum.IsDefined(typeof(Endians), endian))
				{
					endian = BitConverter.IsLittleEndian ? Endians.Little : Endians.Big;
				}

				offset += sizeof(Endians);

				string userToken = null;

				if (offset < data.Length)
				{
					int userTokenSize = BitConverter.ToInt32(data, offset);

					offset += sizeof(int);

					if (userTokenSize > 0)
					{
						userToken = Encoding.UTF8.GetString(data, offset, userTokenSize);
					}
				}

				condition = new AcceptCondition(id, endian, userToken);

				clientType = ClientType.Managed;

				return true;
			}
			catch (Exception error)
			{
				if (!(error is AcceptConditionParseException))
				{
					throw new AcceptConditionParseException("Client is not acceptable. Refer inner exception for the reason.", AcceptConditionParseResult.Unknown, error);
				}
				else
				{
					throw error;
				}
			}
		}

		#endregion

		#region 변환

		/// <summary>
		/// Accept 조건을 바이트 배열로 변환한다.
		/// </summary>
		/// <param name="isLittleEndian"></param>
		/// <returns>Accept 조건이 바이트 배열로 변환되어 반환된다.</returns>
		internal byte[] ToByteArray(bool isLittleEndian)
		{
			byte[] result;

			using (MemoryStream stream = new MemoryStream())
			{
				stream.Write(_ACCEPT_HEADER, 0, _ACCEPT_HEADER.Length);

				byte[] idBinary = Encoding.UTF8.GetBytes(this.Id.ToString());
				
				stream.Write(idBinary, 0, idBinary.Length);

				stream.WriteByte((byte)this.Endian);

				if (this.UserToken != null)
				{
					byte[] userTokenBinary = Encoding.UTF8.GetBytes(this.UserToken.ToString());

					byte[] userTokenSizeBinary = BitConverter.GetBytes(isLittleEndian ? userTokenBinary.Length : IPAddress.HostToNetworkOrder(userTokenBinary.Length));

					stream.Write(userTokenSizeBinary, 0, userTokenSizeBinary.Length);

					stream.Write(userTokenBinary, 0, userTokenBinary.Length);
				}

				result = stream.ToArray();
			}

			return result;
		}

		#endregion
	}
}